/** * Copyright © 2011,2013 Konstantin Livitski * * This file is part of n-Puzzle application. n-Puzzle application is free * software; you can redistribute it and/or modify it under the terms of the GNU * General Public License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * n-Puzzle application contains adaptations of artwork covered by the Creative * Commons Attribution-ShareAlike 3.0 Unported license. Please refer to the * NOTICE.md file at the root of this distribution or repository for licensing * terms that apply to that artwork. * * n-Puzzle application is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more * details. * * You should have received a copy of the GNU General Public License along with * n-Puzzle application; if not, see the LICENSE/gpl.txt file of this distribution * or visit <http://www.gnu.org/licenses>. */ package name.livitski.games.puzzle.android; import java.io.File; import java.io.FilenameFilter; import java.io.Serializable; import java.lang.ref.Reference; import java.lang.ref.SoftReference; import java.lang.reflect.Field; import java.lang.reflect.Modifier; import java.util.HashMap; import java.util.Map; import java.util.SortedMap; import java.util.TreeMap; import name.livitski.games.puzzle.android.R; import name.livitski.games.puzzle.android.model.Game; import android.content.Context; import android.graphics.Bitmap; import android.os.Handler; import android.os.Message; import android.util.DisplayMetrics; import android.util.Log; import android.view.Gravity; import android.view.View; import android.view.ViewGroup; import android.widget.AbsListView.LayoutParams; import android.widget.BaseAdapter; import android.widget.FrameLayout; import android.widget.ImageView; import android.widget.ProgressBar; /** * Provides a list of images from <code>/res/drawable</code> * with specifically {@link #IMAGE_FILE_PREFIX prefixed} names. */ public class ImageSource extends BaseAdapter { @Override public synchronized int getCount() { return images.length; } @Override public synchronized ImageWithConstraints getItem(int index) { return images[index]; } @Override public long getItemId(int index) { return index; } /** * You must call {@link #setContext(Context)} first to set the context * for displaying images. */ @Override public synchronized View getView(final int position, View convertView, ViewGroup parent) { View view = images[position].getView(); return view; } public BaseAdapter userImageSource() { if (null == userImageSource) userImageSource = new BaseAdapter() { @Override public View getView(int position, View convertView, ViewGroup parent) { synchronized (ImageSource.this) { if (position >= getCount()) throw new IndexOutOfBoundsException("User image # " + position + " > " + getCount()); return ImageSource.this.getView(position, convertView, parent); } } @Override public long getItemId(int position) { synchronized (ImageSource.this) { if (position >= getCount()) throw new IndexOutOfBoundsException("User image # " + position + " > " + getCount()); return ImageSource.this.getItemId(position); } } @Override public Object getItem(int position) { synchronized (ImageSource.this) { if (position >= getCount()) throw new IndexOutOfBoundsException("User image # " + position + " > " + getCount()); return ImageSource.this.getItem(position); } } @Override public int getCount() { synchronized (ImageSource.this) { return userImageIndexes.size(); } } }; return userImageSource; } public static File imageFileForId(String id, Context context) { final File dir = context.getFilesDir(); return new File(dir, id); } public synchronized String[] imageFileIds() { final File dir = context.getFilesDir(); return dir.list(new FilenameFilter() { @Override public boolean accept(File dir, String filename) { return filename.startsWith(IMAGE_FILE_PREFIX); } }); } public void setContext(Activity context) { this.context = context; } /** * Loads images into this container. You must call * {@link #setContext(Context)} first to set the context * for loading images. */ public synchronized void init() { if (null != images) return; // store images sorted by their file name suffix SortedMap<String, Integer> builtinImageIds = new TreeMap<String, Integer>(); for (Field field : R.drawable.class.getFields()) try { if (Modifier.STATIC == (field.getModifiers() & Modifier.STATIC) && Integer.TYPE == field.getType() && field.getName().startsWith(IMAGE_FILE_PREFIX)) { String suffix = field.getName().substring(IMAGE_FILE_PREFIX.length()); int id = field.getInt(null); builtinImageIds.put(suffix, id); } } catch (IllegalAccessException skipNoAccess) {} images = new ImageEntry[builtinImageIds.size()]; int i = 0; // add built-in images for (Map.Entry<String, Integer> entry : builtinImageIds.entrySet()) { Integer id = entry.getValue(); Integer difficulty = null; try { difficulty = Integer.valueOf(entry.getKey()); } catch (NumberFormatException ignored) {} ImageEntry image; if (null != difficulty && 0 <= difficulty && 3 > difficulty) image = new ImageEntry(id, difficulty + 3); else image = new ImageEntry(id); images[i++] = image; } updateUserImages(); } /** * Rescans the data directory picking up user image * changes. This method assumes that {@link #init()} * has been called before on the object. */ public synchronized void updateUserImages() { int builtinImageCount = images.length; if (null != userImageIndexes) builtinImageCount -= userImageIndexes.size(); userImageIndexes = new HashMap<String, Integer>(ImageSource.MAX_USER_IMAGE_COUNT, 1f); final String[] userImageIds = imageFileIds(); ImageEntry[] oldImages = images; images = new ImageEntry[userImageIds.length + builtinImageCount]; System.arraycopy( oldImages, oldImages.length - builtinImageCount, images, userImageIds.length, builtinImageCount); int i = 0; // add user's images first for (String id : userImageIds) { userImageIndexes.put(id, i); images[i++] = new ImageEntry(id); } } public synchronized void onImageUpdate(String userImageId) { Integer index = userImageIndexes.get(userImageId); if (null != index) images[index].updateImage(); else updateUserImages(); } private int getMaxThumbnailSize() { final LayoutParams size = getThumbnailSize(); return size.width > size.height ? size.width : size.height; } private LayoutParams getThumbnailSize() { if (null == thumbnailSize) { DisplayMetrics metrics = new DisplayMetrics(); context.getWindowManager().getDefaultDisplay().getMetrics(metrics); thumbnailSize = new LayoutParams( (int)(metrics.density * GRID_CELL_WIDTH_DP), (int)(metrics.density * GRID_CELL_HEIGHT_DP) ); } return thumbnailSize; } /** * Image file name prefix: <code>puzzle_</code> */ public static final String IMAGE_FILE_PREFIX = "puzzle_"; /** * Image file name suffix for user images in the cache. */ public static final String IMAGE_FILE_SUFFIX = ".img"; public static final int MAX_USER_IMAGE_COUNT = 3; protected static final int GRID_CELL_WIDTH_DP = 140; protected static final int GRID_CELL_HEIGHT_DP = 100; interface ImageWithConstraints { /** Resource id or file name of the image. */ Serializable getImageId(); /** * Initial game complexity associated with this image. * <code>null</code> if this image imposes no complexity * requirements on the game. */ Game.Level getInitialComplexity(); } private class ImageEntry implements ImageWithConstraints { @Override public Serializable getImageId() { return id; } @Override public Game.Level getInitialComplexity() { return initialComplexity; } public View getView() { if (null == frame) initView(); return frame; } public void updateImage() { this.thumbnailCache = null; this.frame = null; } public ImageEntry(Serializable id) { this.id = id; } public ImageEntry(Serializable id, int initialBoardSize) { this.id = id; this.initialComplexity = Game.Level.forBoardSize(initialBoardSize); } private void initView() { frame = new FrameLayout(context); frame.setLayoutParams(getThumbnailSize()); Bitmap thumbnail = getThumbnailCache(); if (null != thumbnail) showThumbnail(thumbnail); else { showProgress(); if (null == conversionJob) beginConversion(); } } private void showProgress() { ProgressBar view = new ProgressBar(context); FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); params.gravity = Gravity.CENTER; view.setIndeterminate(true); frame.addView(view, params); } private void showThumbnail(Bitmap thumbnail) { ImageView view = new ImageView(context); FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(getThumbnailSize()); params.gravity = Gravity.CENTER; view.setImageBitmap(thumbnail); frame.addView(view, params); } private void beginConversion() { final Serializable imageId = getImageId(); final Handler thumbnailHandler = new Handler() { @Override public void handleMessage(Message result) { final ImageConverter converter; synchronized (ImageSource.this) { converter = conversionJob; conversionJob = null; } final Throwable status = converter.getStatus(); if (null != status) { String msg = context.getResources().getString(R.string.image_load_error); Log.e(LOG_TAG, msg, status); context.alert(msg); } else { Bitmap thumbnail = converter.getBitmap(); synchronized (ImageSource.this) { setThumbnailCache(thumbnail); if (null != frame) { frame.removeAllViews(); showThumbnail(thumbnail); } } } } }; final ImageConverter converter = new ImageConverter(context, thumbnailHandler); converter.setImageId(imageId); converter.setMaxFrameDimension(getMaxThumbnailSize()); this.conversionJob = converter; context.submitBackgroundTask(converter); } private Bitmap getThumbnailCache() { // Allow the GC to dispose of thumbnails when low on memory, reload them when needed Bitmap thumbnail = null == this.thumbnailCache ? null : this.thumbnailCache.get(); return thumbnail; } private void setThumbnailCache(Bitmap thumbnail) { this.thumbnailCache = new SoftReference<Bitmap>(thumbnail); } private Serializable id; private Game.Level initialComplexity; private Reference<Bitmap> thumbnailCache; private ImageConverter conversionJob; private FrameLayout frame; } private static final String LOG_TAG = "ImageSource"; private Activity context; private BaseAdapter userImageSource; private ImageEntry[] images; private Map<String, Integer> userImageIndexes; private LayoutParams thumbnailSize; }